﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Globalization;
using LumiSoft.Net.Mime;
using Elentok.Common;
using System.Diagnostics;
using Ionic.Zip;
using System.ComponentModel;
using AMail.Common.Interfaces.Data;
using AMail.Common;
using Elentok.Common.Helpers;
using Elentok.Common.Async;

namespace AMail.Modules.Data.Internals
{
  internal class MailArchive : IMailArchive
  {
    #region Private Data Members
    private List<IMailAction> actions = new List<IMailAction>();

    /// <summary>
    /// when this field is set true no actions will be registered
    /// </summary>
    private bool suspendActions = true;
    #endregion

    #region Consts
    private const string IndexFileName = "index";
    private const string HasUnsavedChangesPropertyName = "HasUnsavedChanges";
    #endregion

    #region Ctor
    public MailArchive(string filename, string passphrase)
    {
      this.Passphrase = passphrase;
      this.Filename = filename;

      if (File.Exists(filename))
        LoadFromFile();
      else
        Root = new RootMailFolder(this);

      Root.ActionPerformed += new EventHandler<MailActionEventArgs>(Root_ActionPerformed);

      this.suspendActions = false;
    }
    #endregion

    #region Event Handlers
    private void Root_ActionPerformed(object sender, MailActionEventArgs e)
    {
      if (this.suspendActions)
        return;

      this.actions.Add(e.Action);
      OnPropertyChanged(HasUnsavedChangesPropertyName);
    }

    #endregion

    #region IMailArchive Members
    public IMailFolder Root { get; private set; }
    public string Filename { get; private set; }
    public string Passphrase { get; private set; }
    public bool HasUnsavedChanges
    {
      get { return this.actions.Count > 0 || !File.Exists(this.Filename); }
    }

    /// <summary>
    /// Saves the pending changes in this archive
    /// </summary>
    /// <param name="progressNotifier"></param>
    /// <exception cref="SaveArchiveException"></exception>
    public void Save(IAsyncProgressNotifier progressNotifier)
    {
      CommitChanges(progressNotifier);
    }

    public byte[] ExtractMessageSource(IMailMessage message)
    {
      using (ZipFile file = new ZipFile(Filename))
      {
        ZipEntry entry = file[message.Id];
        using (Stream encryptedStream = entry.OpenReader())
        {
          if (string.IsNullOrEmpty(this.Passphrase))
            return encryptedStream.ReadAllBytes();

          using (MemoryStream plainTextStream = encryptedStream.AesDecrypt(Passphrase))
          {
            byte[] bytes = plainTextStream.ToArray();
            BinaryHelper.TrimRightZeroPadding(ref bytes);
            return bytes;
          }
        }
      }
    }
    #endregion

    #region Save

    private byte[] Serialize()
    {
      MemoryStream xmlStream = new MemoryStream();
      XmlTextWriter xmlWriter = new XmlTextWriter(xmlStream, Encoding.UTF8);
      xmlWriter.Formatting = Formatting.Indented;
      Root.WriteXml(xmlWriter);
      xmlWriter.Flush();
      xmlStream.Seek(0, SeekOrigin.Begin);

      return xmlStream.ToArray();
    }

    private void CommitChanges(IAsyncProgressNotifier progressNotifier)
    {
      using (ZipFile file = new ZipFile(Filename, Encoding.UTF8))
      {
        // +11 is 1 for the index file and +10 for the save itself
        progressNotifier.NotifyProgress(null, "Encrypting index file...", null);

        #region Index File
        if (file.ContainsEntry(IndexFileName))
          file.RemoveEntry(IndexFileName);

        byte[] encryptedIndex = EncryptData(Serialize());
        file.AddEntry(IndexFileName, encryptedIndex);
        #endregion

        progressNotifier.NotifyProgress(null, "Saving...", null);

        var archiver = new MailArchiver(file, this);

        foreach (var action in this.actions)
          action.Commit(archiver);

        progressNotifier.NotifyProgress(null, "Commiting changes...", null);
        archiver.Commit();

        progressNotifier.NotifyProgress(null, "Saving...", null);
        try
        {
          file.Save();
        }
        catch (UnauthorizedAccessException ex)
        {
          throw new SaveArchiveException(ex.Message, ex);
        }
        catch (Exception ex)
        {
          throw new SaveArchiveException("Unknown error while trying to save", ex);
        }
        
        // now that the file was saved, we can clear the commited actions
        this.actions.Clear();

        archiver.AfterSave();
        OnPropertyChanged(HasUnsavedChangesPropertyName);
      }
    }

    public void Undo()
    {
      this.suspendActions = true;

      try
      {
        if (this.actions.Count == 0)
          return;

        var lastIndex = this.actions.Count - 1;
        var lastAction = this.actions[lastIndex];
        this.actions.RemoveAt(lastIndex);
        lastAction.Undo();
      }
      finally
      {
        if (this.actions.Count == 0)
          OnPropertyChanged(HasUnsavedChangesPropertyName);

        this.suspendActions = false;
      }
    }

    //private void SaveNewMessages(ZipFile file, IProgressNotifier progressNotifier)
    //{
    //  List<IMailMessage> messagesToSave = new List<IMailMessage>();
    //  Root.FindMessages(m => m.IsNew, messagesToSave);

    //  int index = 0;
    //  foreach (IMailMessage message in messagesToSave)
    //  {
    //    if (progressNotifier != null)
    //      progressNotifier.NotifyProgress(index * 100 / messagesToSave.Count, "Saving...");

    //    byte[] encryptedMessage = CryptoUtils.AesEncrypt(message.Source, Passphrase);
    //    file.AddEntry(message.Id, encryptedMessage);
    //    index++;
    //  }
    //}

    //private void SaveExisting(IProgressNotifier progressNotifier)
    //{
    //  using (ZipFile file = new ZipFile(Filename))
    //  {
    //    if (progressNotifier != null)
    //      progressNotifier.NotifyProgress(0, "Saving index...");

    //    #region Replace the index entry
    //    byte[] encryptedIndex = Serialize();
    //    file.UpdateEntry(IndexFileName, encryptedIndex);
    //    #endregion

    //    int index = 0;
    //    int total = _messageToDelete.Count + _messageToSave.Keys.Count;

    //    #region Delete messages
    //    foreach (MailMessage message in _messageToDelete)
    //    {
    //      if (progressNotifier != null)
    //        progressNotifier.NotifyProgress(index++ * 100 / total, "Removing deleted messages...");

    //      file.RemoveEntry(message.Id);
    //    }
    //    #endregion

    //    SaveNewMessages(file, progressNotifier);

    //    file.Save();
    //  }

    //  _messageToSave.Clear();
    //  _messageToDelete.Clear();

    //  HasUnsavedChanges = false;
    //}
    #endregion

    //#region Folders
    ///// <summary>
    ///// Deletes the given folder and all subfolders and messages (including messages inside subfolders).
    ///// (moves the messages to the "MessagesMarkedForDeletion" list until the "Save" method is called).
    ///// </summary>
    ///// <param name="folder"></param>
    //public void DeleteFolder(MailFolder folder)
    //{
    //  // delete all subfolders
    //  while(folder.Subfolders.Count > 0)
    //    DeleteFolder(folder.Subfolders[0]);

    //  // delete all messages
    //  while (folder.Messages.Count > 0)
    //  {
    //    MailMessage message = folder.Messages[0];

    //    if (_messageToSave.ContainsKey(message))
    //      _messageToSave.Remove(message);
    //    _messageToDelete.Add(message);
    //    folder.RemoveMessage(message);
    //  }

    //  // delete the folder itself
    //  if (folder.ParentFolder != null)
    //    folder.ParentFolder.RemoveSubfolder(folder);

    //  OnChanged(MailArchiveChange.FolderDeleted, folder);
    //}
    //#endregion

    //#region Messages

    //public void DeleteMessages(ReadOnlyCollection<MailMessage> messages)
    //{
    //  foreach (MailMessage message in messages)
    //    message.ParentFolder.RemoveMessage(message);

    //  _messageToDelete.AddRange(messages);
    //  OnChanged(MailArchiveChange.MessageDeleted, messages);
    //}

    //#endregion

    #region Change Password
    /// <summary>
    /// Modifies the password of the current file
    /// </summary>
    /// <param name="newPassphrase"></param>
    /// <param name="progressNotifier"></param>
    public void ChangePassword(string newPassphrase, IAsyncProgressNotifier progressNotifier)
    {
      string tempFile = Path.ChangeExtension(Filename, ".new-password.amail");
      if (File.Exists(tempFile))
        File.Delete(tempFile);
      IMailArchive newArchive = new MailArchive(tempFile, newPassphrase);
      int totalMessages = Root.CountMessages();

      Root.CopyFolderTo(newArchive.Root, totalMessages, 0, progressNotifier);
      newArchive.Save(progressNotifier);

      string oldFile = Path.ChangeExtension(Filename, ".old-password.amail");
      if (File.Exists(oldFile))
        File.Delete(oldFile);
      File.Move(Filename, oldFile);
      File.Move(newArchive.Filename, Filename);
      Passphrase = newPassphrase;
    }

    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Raises the PropertyChanged event
    /// </summary>
    protected void OnPropertyChanged(string propertyName)
    {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion

    #region IMailArchive Members


    #endregion

    #region Helper Methods
    private XElement GetIndexXmlElement(ZipFile zipFile)
    {
      ZipEntry indexEntry = zipFile["index"];
      using (Stream encryptedStream = indexEntry.OpenReader())
      {
        if (string.IsNullOrEmpty(this.Passphrase))
        {
          XmlTextReader reader = null;
          try
          {
            reader = new XmlTextReader(encryptedStream);
            return XElement.Load(reader);
          }
          catch (Exception e)
          {
            throw new WrongPasswordException("", e);
          }
          finally
          {
            if (reader != null)
              reader.Close();
          }
        }
        else
        {
          using (MemoryStream plainTextStream = encryptedStream.AesDecrypt(this.Passphrase))
          {
            if (!plainTextStream.IsUtf8())
              throw new WrongPasswordException();

            using (XmlTextReader reader = new XmlTextReader(plainTextStream))
            {
              return XElement.Load(reader);
            }
          }
        }
      }
    }

    private void LoadFromFile()
    {
      using (ZipFile file = new ZipFile(this.Filename))
      {
        XElement element = GetIndexXmlElement(file);
        Root = new RootMailFolder(this, element);
      }
    }

    /// <summary>
    /// If there's a password the data will be encrypted,
    /// otherwise it will just be returned
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    internal byte[] EncryptData(byte[] data)
    {
      if (string.IsNullOrEmpty(this.Passphrase))
        return data;
      else
        return data.AesEncrypt(this.Passphrase);
    }

    #endregion

    private class MailArchiver : IMailArchiver
    {
      private ZipFile zipFile;
      private MailArchive archive;

      private List<IMailMessage> messagesToAdd = new List<IMailMessage>();
      private List<IMailMessage> messagesToDelete = new List<IMailMessage>();

      public MailArchiver(ZipFile zipFile, MailArchive archive)
      {
        this.zipFile = zipFile;
        this.archive = archive;
      }

      #region IMailArchiver Members

      public void AddMessage(IMailMessage message)
      {
        if (this.messagesToDelete.Contains(message))
          this.messagesToDelete.Remove(message);

        if (!this.messagesToAdd.Contains(message))
          this.messagesToAdd.Add(message);
      }

      public void DeleteMessage(IMailMessage message)
      {
        if (this.messagesToAdd.Contains(message))
          this.messagesToAdd.Remove(message);

        if (!message.IsNew && !this.messagesToDelete.Contains(message))
          this.messagesToDelete.Add(message);
      }

      public void Commit()
      {
        foreach (var message in messagesToDelete)
        {
          zipFile.RemoveEntry(message.Id);
        }

        foreach (var message in messagesToAdd)
        {
          byte[] encryptedMessage = this.archive.EncryptData(message.Source);
          zipFile.AddEntry(message.Id, encryptedMessage);
        }
      }

      public void AfterSave()
      {
        foreach (var message in messagesToAdd)
        {
          var internalMessage = message as IInternalMailMessage;
          if (internalMessage != null)
            internalMessage.MarkAsSaved();
        }
      }

      #endregion
    }
  }

  //public class MailArchiveChangedEventArgs : EventArgs
  //{
  //  public MailArchiveChange WhatChanged { get; private set; }
  //  public MailFolder Folder { get; private set; }
  //  public ReadOnlyCollection<MailMessage> Messages { get; private set; }

  //  public MailArchiveChangedEventArgs(MailArchiveChange whatChanged, MailFolder folder)
  //  {
  //    this.WhatChanged = whatChanged;
  //    this.Folder = folder;
  //  }
  //  public MailArchiveChangedEventArgs(MailArchiveChange whatChanged, ReadOnlyCollection<MailMessage> messages)
  //  {
  //    this.WhatChanged = whatChanged;
  //    this.Messages = messages;
  //  }
  //  public MailArchiveChangedEventArgs(MailArchiveChange whatChanged, MailMessage message)
  //  {
  //    this.WhatChanged = whatChanged;

  //    List<MailMessage> messages = new List<MailMessage>();
  //    messages.Add(message);
  //    this.Messages = messages.AsReadOnly();
  //  }

  //}

  //public enum MailArchiveChange
  //{
  //  FolderAdded,
  //  FolderDeleted,
  //  FolderRenamed,
  //  MessageAdded,
  //  MessageDeleted,
  //  MessagesMoved
  //}
}
